package org.teremail.store.paged;

import static org.teremail.schema.DBTypes.INT4;
import static org.teremail.schema.DBTypes.INT8;
import static org.teremail.schema.DBTypes.VARCHAR;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;

import javax.sql.DataSource;

import org.teremail.common.Log;
import org.teremail.schema.DBTypes;
import org.teremail.schema.Dialect;
import org.teremail.schema.Schema;
import org.teremail.schema.Table;
import org.teremail.schema.TableBuilder;
import org.teremail.service.Serviceable;
import org.teremail.service.ServiceAdapter;
import org.teremail.service.ServiceRegistrar;
import org.teremail.store.StoreException;
import org.teremail.util.DB;

import com.google.inject.Inject;

/**
 * Implements a database implementation of the blob factory.
 * 
 * @author Michael Barker
 *
 */
public class DBBlobFactory implements BlobFactory, Serviceable {

    private final static Log log = Log.getLog(BlobFactory.class);
    public final static Table PAGE_TABLE;
    public final static Table BLOB_TABLE;
    
    private final DataSource ds;
    private int pageSize = 8192;
    private final Dialect dialect;
    
    static {
        TableBuilder blobTB = new TableBuilder("PAGED_STORE");
        blobTB.column("ID", VARCHAR(40)).primaryKey().nullable(false);
        blobTB.column("PAGE_SIZE", INT4()).nullable(false);
        blobTB.column("LEN", INT8()).nullable(false);
        BLOB_TABLE = blobTB.build();
        
        TableBuilder pageTB = new TableBuilder("PAGED_STORE_PAGE");
        pageTB.column("BLOB_ID", VARCHAR(40)).primaryKey().nullable(false);
        pageTB.column("PAGE_NO", INT4()).primaryKey().nullable(false);
        pageTB.column("DATA", DBTypes.BINARY(8192)).nullable(false);
        PAGE_TABLE = pageTB.build();
    }

    @Inject
    public DBBlobFactory(DataSource ds, Dialect dialect) {
        this.ds = ds;
        this.dialect = dialect;
    }
    
    public Blob createBlob() {
        String id = UUID.randomUUID().toString();
        insertBlob(id, pageSize);
        return new DBBlob(id, pageSize, 0L, 0);
    }
    
    private void insertBlob(String id, int pageSize) {
        Connection cn = null;
        PreparedStatement ps = null;
        try {
            cn = ds.getConnection();
            ps = cn.prepareStatement("INSERT INTO PAGED_STORE (ID, PAGE_SIZE, LEN) VALUES (?, ?, 0)");
            ps.setString(1, id);
            ps.setInt(2, pageSize);
            ps.execute();
        } catch (SQLException e) {
            throw new StoreException(e);
        } finally {
            DB.close(ps, cn);
        }
    }
    
    private DBBlob selectBlob(String id) {
        Connection cn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            cn = ds.getConnection();
            ps = cn.prepareStatement("SELECT PAGE_SIZE, LEN, (SELECT COUNT(PAGE_NO) FROM PAGED_STORE_PAGE WHERE BLOB_ID = ?) FROM PAGED_STORE WHERE ID = ?");
            ps.setString(1, id);
            ps.setString(2, id);
            rs = ps.executeQuery();
            if (rs.next()) {
                int pageSize = rs.getInt(1);
                long len = rs.getLong(2);
                int numPages = rs.getInt(3);
                return new DBBlob(id, pageSize, len, numPages);
            } else {
                throw new StoreException("Blob %s not found", id);
            }
        } catch (SQLException e) {
            throw new StoreException(e);
        } finally {
            DB.close(rs, ps, cn);
        }
    }
    
    private void insertPage(String id, int pageNo, byte[] data, long length) {
        Connection cn = null;
        PreparedStatement psPage = null;
        PreparedStatement psBlob = null;
        try {
            cn = ds.getConnection();
            psPage = cn.prepareStatement("INSERT INTO PAGED_STORE_PAGE (BLOB_ID, PAGE_NO, DATA) VALUES (?, ?, ?)");
            psPage.setString(1, id);
            psPage.setInt(2, pageNo);
            psPage.setBytes(3, data);
            psPage.executeUpdate();
            
            psBlob = cn.prepareStatement("UPDATE PAGED_STORE SET LEN = ? WHERE ID = ?");
            psBlob.setLong(1, length);
            psBlob.setString(2, id);
            psBlob.executeUpdate();
        } catch (SQLException e) {
            throw new StoreException(e);
        } finally {
            DB.close(psPage);
            DB.close(psBlob, cn);
        }
    }
    
    private byte[] selectPage(String id, int pageNo) {
        
        log.info("selectPage Blob: %s, Page: %d", id, pageNo);        
        
        Connection cn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            cn = ds.getConnection();
            ps = cn.prepareStatement("SELECT DATA FROM PAGED_STORE_PAGE WHERE BLOB_ID = ? AND PAGE_NO = ?");
            ps.setString(1, id);
            ps.setInt(2, pageNo);
            rs = ps.executeQuery();
            if (rs.next()) {
                return rs.getBytes(1);
            } else {
                throw new StoreException("Blob %s, Page %d not found", id, pageNo);
            }
        } catch (SQLException e) {
            throw new StoreException(e);
        } finally {
            DB.close(rs, ps, cn);
        }
    }
    
    private void updatePage(String id, int pageNo, byte[] data) {
        
        log.info("updatePage Blob: %s, Page: %d, Data Length: %d", id, pageNo, data.length);
        
        Connection cn = null;
        PreparedStatement ps = null;
        try {
            cn = ds.getConnection();
            ps = cn.prepareStatement("UPDATE PAGED_STORE_PAGE SET DATA = ? WHERE BLOB_ID = ? AND PAGE_NO = ?");
            ps.setBytes(1, data);
            ps.setString(2, id);
            ps.setInt(3, pageNo);
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new StoreException(e);
        } finally {
            DB.close(ps, cn);
        }
    }
    

    public DBBlob getBlob(String id) {
        return selectBlob(id);
    }
    
    private class DBBlob implements Blob {

        private final String id;
        private final int pageSize;
        private long length = 0;
        private int numPages;

        public DBBlob(String id, int pageSize, long length, int numPages) {
            this.id = id;
            this.pageSize = pageSize;
            this.length = length;
            this.numPages = numPages;
        }

        public void addPage(byte[] bs) {
            insertPage(id, numPages, bs, length + bs.length);
            
            numPages++;
            length += bs.length;            
        }

        public String getId() {
            return id;
        }

        public byte[] getLastPage() {
            return getPage(numPages - 1);
        }

        public long getLength() {
            return length;
        }

        public int getNumPages() {
            return numPages;
        }

        public byte[] getPage(int i) {
            return selectPage(id, i);
        }

        public int getPageSize() {
            return pageSize;
        }

        public void replaceLastPage(byte[] newData) {
            updatePage(id, numPages - 1, newData);
        }
        
    }
        
    // ==================
    // Service Management
    // ==================
    
    public void init() {
        Schema.createIfNotExists(ds, BLOB_TABLE, dialect);
        Schema.createIfNotExists(ds, PAGE_TABLE, dialect);
    }

    @Inject
    public void registerService(ServiceRegistrar serviceRegistrar) {
        serviceRegistrar.register(new ServiceAdapter(){
            @Override
            public void start() throws Exception {
                init();
            }
        });
    }
}
